/*
 * DBeaver - Universal Database Manager
 * Copyright (C) 2010-2025 DBeaver Corp and others
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jkiss.dbeaver.ui.controls;

import org.eclipse.jface.fieldassist.ComboContentAdapter;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.*;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.*;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.*;
import org.jkiss.dbeaver.model.data.DBDDisplayFormat;
import org.jkiss.dbeaver.model.edit.DBECommandContext;
import org.jkiss.dbeaver.model.edit.DBEObjectManager;
import org.jkiss.dbeaver.model.edit.DBEObjectRenamer;
import org.jkiss.dbeaver.model.meta.IPropertyValueListProvider;
import org.jkiss.dbeaver.model.meta.PropertyLength;
import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor;
import org.jkiss.dbeaver.model.preferences.DBPPropertySource;
import org.jkiss.dbeaver.model.struct.DBSObject;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.dbeaver.runtime.properties.ObjectPropertyDescriptor;
import org.jkiss.dbeaver.ui.*;
import org.jkiss.dbeaver.ui.contentassist.ContentAssistUtils;
import org.jkiss.dbeaver.ui.contentassist.StringContentProposalProvider;
import org.jkiss.dbeaver.ui.dialogs.EditTextDialog;
import org.jkiss.dbeaver.ui.internal.UIMessages;
import org.jkiss.dbeaver.utils.GeneralUtils;
import org.jkiss.utils.BeanUtils;
import org.jkiss.utils.CommonUtils;

import java.util.*;
import java.util.List;

/**
 * CustomFormEditor
 */
public class CustomFormEditor {
    private static final Log log = Log.getLog(CustomFormEditor.class);

    private static final String VALUE_KEY = "form.data.value";
    private static final String LIST_VALUE_KEY = "form.data.list.value";

    private final Map<DBPPropertyDescriptor, Control> editorMap = new HashMap<>();
    @Nullable
    private final DBSObject databaseObject;
    @Nullable
    private final DBECommandContext commandContext;
    @NotNull
    private final DBPPropertySource propertySource;

    private Composite curButtonsContainer;
    private transient boolean isLoading;

    ///////////////////////////////////////////////
    //

    public CustomFormEditor(
        @NotNull DBSObject databaseObject,
        @Nullable DBECommandContext commandContext,
        @NotNull DBPPropertySource propertySource
    ) {
        this.databaseObject = databaseObject;
        this.commandContext = commandContext;
        this.propertySource = propertySource;
    }

    protected void openObjectLink(Object linkData) {
        if (linkData != null) {
            UIUtils.openWebBrowser(CommonUtils.toString(linkData));
        }
    }

    ////////////////////////////////////////////////
    //

    public void updateOtherPropertyValues(@Nullable Object excludePropId) {
        List<DBPPropertyDescriptor> allProps = filterProperties(propertySource.getProperties());

        Map<DBPPropertyDescriptor, Object> propValues = new HashMap<>();
        for (DBPPropertyDescriptor prop : allProps) {
            if (excludePropId != null && excludePropId.equals(prop.getId())) {
                continue;
            }
            Object value = propertySource.getPropertyValue(null, prop.getId());
            propValues.put(prop, value);
        }
        loadEditorValues(propValues);
    }

    public boolean isEditableObject() {
        for (DBPPropertyDescriptor prop : propertySource.getProperties()) {
            if (prop.isEditable(propertySource.getEditableValue()) ||
                (prop.getId().equals(DBConstants.PROP_ID_NAME) && supportsObjectRename()))
            {
                return true;
            }
        }
        return false;
    }

    private boolean supportsObjectRename() {
        return DBWorkbench.getPlatform().getEditorsRegistry().getObjectManager(
            propertySource.getEditableValue().getClass(), DBEObjectRenamer.class) != null;
    }

    public void createPropertyEditor(Composite group, DBPPropertyDescriptor prop) {

        isLoading = true;

        try {
            boolean isReadOnlyCon = databaseObject == null || DBUtils.isReadOnly(databaseObject);
            if (prop == null) {
                UIUtils.createEmptyLabel(group, 2, 1);
            } else {
                boolean editable = !isReadOnlyCon && (prop.isEditable(propertySource.getEditableValue()) ||
                    (prop.getId().equals(DBConstants.PROP_ID_NAME) && supportsObjectRename()));
                Object propertyValue = propertySource.getPropertyValue(null, prop.getId());
                if (propertyValue == null && prop instanceof ObjectPropertyDescriptor && ((ObjectPropertyDescriptor) prop).isOptional()) {
                    // Do not create editor for null optional properties
                    return;
                }
                Control editControl = createEditorControl(
                    group,
                    propertySource.getEditableValue(),
                    prop,
                    propertyValue,
                    !editable);
                String propDescription = prop.getDescription();
                if (!CommonUtils.isEmpty(propDescription)) {
                    editControl.setToolTipText(propDescription);
                }
                if (editControl instanceof Button) {
                    // nothing
                } else {
                    //boolean plainText = (CharSequence.class.isAssignableFrom(propType));
                    GridData gd = (GridData) editControl.getLayoutData();
                    if (gd == null) {
                        gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_BEGINNING);
                        editControl.setLayoutData(gd);
                    }
                    if (editControl instanceof Text || editControl instanceof StyledText || editControl instanceof Combo) {
                        gd.widthHint = Math.max(
                            UIUtils.getFontHeight(group) * 15,
                            editControl.computeSize(SWT.DEFAULT, SWT.DEFAULT).x);
                    }
                }

                editorMap.put(prop, editControl);

                if (editable) {
                    if (editControl instanceof Combo combo) {
                        if ((editControl.getStyle() & SWT.READ_ONLY) == SWT.READ_ONLY) {
                            combo.addSelectionListener(new SelectionAdapter() {
                                @Override
                                public void widgetSelected(SelectionEvent e) {
                                    updatePropertyValue(prop, combo.getText());
                                }
                            });
                        } else {
                            combo.addModifyListener(e -> {
                                try {
                                    updatePropertyValue(prop, combo.getText());
                                } catch (Exception ex) {
                                    log.debug("Error setting value from combo: " + ex.getMessage());
                                }
                            });
                        }
                    } else if (editControl instanceof Text text) {
                        text.addModifyListener(e -> updatePropertyValue(prop, text.getText()));
                    } else if (editControl instanceof StyledText text) {
                        text.addModifyListener(e -> updatePropertyValue(prop, text.getText()));
                    } else if (editControl instanceof Button button) {
                        button.addSelectionListener(new SelectionAdapter() {
                            @Override
                            public void widgetSelected(SelectionEvent e) {
                                updatePropertyValue(prop, button.getSelection());
                            }
                        });
                    }
                }
            }
        } finally {
            isLoading = false;
        }
    }

    private void updatePropertyValue(@NotNull DBPPropertyDescriptor prop, @Nullable Object value) {
        if (!isLoading) {
            if (prop.getId().equals(DBConstants.PROP_ID_NAME) && databaseObject != null && databaseObject.isPersisted()) {
                DBEObjectRenamer renamer = DBWorkbench.getPlatform().getEditorsRegistry().getObjectManager(
                    propertySource.getEditableValue().getClass(), DBEObjectRenamer.class);
                if (commandContext != null && renamer != null) {
                    try {
                        Map<String, Object> options = new LinkedHashMap<>();
                        options.put(DBEObjectManager.OPTION_UI_SOURCE, this);
                        renamer.renameObject(
                            commandContext,
                            databaseObject,
                            options,
                            CommonUtils.toString(UIUtils.normalizePropertyValue(value))
                        );
                    } catch (Throwable e) {
                        log.error("Error renaming object", e);
                    }
                }
            } else {
                Class<?> dataType = prop.getDataType();
                if (value instanceof String) {
                    value = GeneralUtils.convertString((String) UIUtils.normalizePropertyValue(value), dataType);
                }
                Object oldPropValue = propertySource.getPropertyValue(null, prop.getId());
                propertySource.setPropertyValue(null, prop.getId(), value);
            }
        }
    }

    /**
     * Supported editors:
     * Combo (lists)
     * Text (strings, numbers, dates)
     * Button (booleans)
     */
    public Control createEditorControl(
        @NotNull Composite parent,
        @NotNull Object object,
        @NotNull DBPPropertyDescriptor property,
        @Nullable Object value,
        boolean readOnly
    ) {
        // List
        String propertyDisplayName = property.getDisplayName();
        if (property.isRequired()) {
            propertyDisplayName += " (*)";
        }
        if (!readOnly && property instanceof IPropertyValueListProvider listProvider) {
            Object[] items = listProvider.getPossibleValues(object);
            if (items == null && property instanceof ObjectPropertyDescriptor opd && opd.hasListValueProvider()) {
                // It is a list provider but it seems to be lazy and not yet initialized
                items = new Object[0];
            }
            if (items != null) {
                List<String> strings = new ArrayList<>(items.length);
                for (Object item : items) {
                    strings.add(objectValueToString(item));
                }
                if (!property.isRequired()) {
                    // Add null value
                    strings.addFirst("");
                }
                String curValue = objectValueToString(value);
                if (!CommonUtils.isEmpty(curValue) && !strings.contains(curValue)) {
                    strings.add(curValue);
                }
                Combo combo = UIUtils.createLabelCombo(
                    parent,
                    propertyDisplayName,
                    SWT.BORDER | SWT.DROP_DOWN | (listProvider.allowCustomValue() ? SWT.NONE : SWT.READ_ONLY)
                );

                String[] stringsArray = strings.toArray(new String[0]);
                combo.setItems(stringsArray);
                combo.setText(curValue);
                combo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_BEGINNING));

                if ((combo.getStyle() & SWT.READ_ONLY) == 0) {
                    StringContentProposalProvider proposalProvider = new StringContentProposalProvider(stringsArray);
                    ContentAssistUtils.installContentProposal(combo, new ComboContentAdapter(), proposalProvider);
                }

                return combo;
            }
        }
        Class<?> propType = property.getDataType();
        if (property.hasFeature(DBConstants.PROP_FEATURE_INFO)) {
            Composite infoComposite = new Composite(parent, SWT.NONE);
            GridLayout layout = new GridLayout(2, false);
            layout.marginHeight = 0;
            layout.marginWidth = 0;
            infoComposite.setLayout(layout);

            Label iconLabel = new Label(infoComposite, SWT.NONE);
            iconLabel.setImage(DBeaverIcons.getImage(DBIcon.SMALL_INFO));
            iconLabel.setLayoutData(new GridData(SWT.BEGINNING, SWT.TOP, false, false));

            Label textLabel = new Label(infoComposite, SWT.WRAP);
            textLabel.setText(objectValueToString(value));
            GridData textGd = new GridData(SWT.FILL, SWT.FILL, true, false);
            textLabel.setLayoutData(textGd);

            GridData gd = new GridData(SWT.FILL, SWT.FILL, true, false);
            gd.horizontalSpan = 2;
            infoComposite.setLayoutData(gd);
            return infoComposite;

        }

        if (DBSObject.class.isAssignableFrom(propType) || isLinkProperty(property)) {
            UIUtils.createControlLabel(
                parent,
                propertyDisplayName);
            Link link = new Link(parent, SWT.NONE);
            link.setText(getLinkTitle(value));
            link.setData(value);
            link.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> openObjectLink(link.getData())));
            link.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
            return link;
        } else if (isTextPropertyType(propType)) {
            if (property instanceof ObjectPropertyDescriptor && property.getLength() == PropertyLength.MULTILINE) {
                Label label = UIUtils.createControlLabel(parent, propertyDisplayName);
                label.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING));

                Text editor = new Text(parent, SWT.MULTI | SWT.WRAP | SWT.BORDER | SWT.V_SCROLL | (readOnly ? SWT.READ_ONLY : SWT.NONE));
                editor.setText(objectValueToString(value));

                GridDataFactory.fillDefaults()
                    .align(SWT.FILL, SWT.TOP)
                    .grab(true, true)
                    .hint(SWT.DEFAULT, UIUtils.getTextHeight(editor) * 3)
                    .applyTo(editor);

                HoverControlSupport.install(editor, (parent1, editor1) -> {
                    ToolBar toolBar = new ToolBar(parent1, SWT.FLAT);
                    ToolItem toolItem = new ToolItem(toolBar, SWT.PUSH);
                    toolItem.setImage(DBeaverIcons.getImage(UIIcon.EDIT));
                    toolItem.setToolTipText("Edit...");
                    toolItem.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
                        if (readOnly) {
                            EditTextDialog.showText(
                                editor.getShell(),
                                UIMessages.edit_text_dialog_title_edit_value,
                                editor.getText()
                            );
                        } else {
                            String newValue = EditTextDialog.editText(
                                editor.getShell(),
                                UIMessages.edit_text_dialog_title_edit_value,
                                editor.getText()
                            );
                            if (newValue != null) {
                                editor.setText(newValue);
                            }
                        }
                    }));
                });

                return editor;
            } else {
                UIUtils.createControlLabel(parent, propertyDisplayName);
                Text text = new Text(parent, SWT.BORDER |
                    (readOnly ? SWT.READ_ONLY : SWT.NONE) |
                    (property instanceof ObjectPropertyDescriptor && ((ObjectPropertyDescriptor) property).isPassword() ? SWT.PASSWORD : SWT.NONE));
                text.setText(objectValueToString(value));
                text.setLayoutData(new GridData((BeanUtils.isNumericType(propType) ? GridData.HORIZONTAL_ALIGN_BEGINNING : GridData.FILL_HORIZONTAL) | GridData.VERTICAL_ALIGN_BEGINNING));
                return text;
            }
        } else if (BeanUtils.isBooleanType(propType)) {
            if (curButtonsContainer == null) {
                UIUtils.createEmptyLabel(parent, 1, 1);
                curButtonsContainer = new ConComposite(parent, SWT.NONE);
                RowLayout layout = new RowLayout(SWT.HORIZONTAL);
                curButtonsContainer.setLayout(layout);
                GridData gd = new GridData(GridData.FILL_HORIZONTAL);
                curButtonsContainer.setLayoutData(gd);
            }
            Composite bPH = UIUtils.createComposite(curButtonsContainer, 2);
            ((GridLayout)bPH.getLayout()).marginRight = 10;
            Button editor = UIUtils.createCheckbox(
                bPH,
                "",
                "",
                CommonUtils.toBoolean(value),
                1
            );
            Label label = UIUtils.createLabel(bPH, propertyDisplayName);
            label.addMouseListener(new MouseAdapter() {
                @Override
                public void mouseUp(MouseEvent e) {
                    // Handle click on label
                    if (editor.isEnabled()) {
                        editor.setSelection(!editor.getSelection());
                        editor.notifyListeners(SWT.Selection, new Event());
                    }
                }
            });

            if (readOnly) {
                editor.setEnabled(false);
            }
            return editor;
        } else if (!readOnly && propType.isEnum()) {
            final Object[] enumConstants = propType.getEnumConstants();
            final String[] strings = new String[enumConstants.length];
            for (int i = 0, itemsLength = enumConstants.length; i < itemsLength; i++) {
                strings[i] = ((Enum)enumConstants[i]).name();
            }
            Combo combo = UIUtils.createLabelCombo(
                parent,
                propertyDisplayName,
                SWT.BORDER | SWT.DROP_DOWN | SWT.READ_ONLY | SWT.NONE
            );
            combo.setItems(strings);
            combo.setText(objectValueToString(value));
            combo.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING | GridData.VERTICAL_ALIGN_BEGINNING));
            return combo;
        } else {
            return UIUtils.createLabelText(
                parent,
                propertyDisplayName,
                objectValueToString(value),
                SWT.BORDER | SWT.READ_ONLY);
        }
    }

    private static boolean isLinkProperty(@NotNull DBPPropertyDescriptor property) {
        return property instanceof ObjectPropertyDescriptor opd &&
            (opd.isLinkPossible() || opd.isHref());
    }

    private String getLinkTitle(Object value) {
        return value == null ? "N/A" : "<a>" + objectValueToString(value) + "</a>";
    }

    public void loadEditorValues(@NotNull Map<DBPPropertyDescriptor, Object> editorValues) {
        try {
            isLoading = true;
            if (propertySource != null) {
                Object object = propertySource.getEditableValue();
                for (Map.Entry<DBPPropertyDescriptor, Object> prop : editorValues.entrySet()) {
                    setEditorValue(object, prop.getKey(), prop.getValue());
                }
            }
        } finally {
            isLoading = false;
        }
    }

    public void setEditorValue(@NotNull Object object, @NotNull DBPPropertyDescriptor property, @Nullable Object value) {
        Control editorControl = editorMap.get(property);
        Class<?> propertyType = property.getDataType();
        // List
        String stringValue = objectValueToString(value);
        if (editorControl instanceof Combo combo) {
            if (property instanceof IPropertyValueListProvider listProvider) {
                final Object[] items = listProvider.getPossibleValues(object);
                final Object[] oldItems = (Object[]) combo.getData(LIST_VALUE_KEY);
                combo.setData(LIST_VALUE_KEY, items);
                if (items != null) {
                    if (oldItems == null || !Arrays.equals(items, oldItems)) {
                        final String[] strings = new String[items.length];
                        for (int i = 0, itemsLength = items.length; i < itemsLength; i++) {
                            strings[i] = items[i] instanceof DBPNamedObject ? ((DBPNamedObject) items[i]).getName() : CommonUtils.toString(items[i]);
                        }
                        combo.setItems(strings);
                    }
                    combo.setText(stringValue);
                }
            } else if (propertyType != null && propertyType.isEnum()) {
                if (combo.getItemCount() == 0) {
                    // Do not refresh enum values - they are static
                    final Object[] enumConstants = propertyType.getEnumConstants();
                    final String[] strings = new String[enumConstants.length];
                    for (int i = 0, itemsLength = enumConstants.length; i < itemsLength; i++) {
                        strings[i] = ((Enum<?>) enumConstants[i]).name();
                    }
                    combo.setItems(strings);
                }
                combo.setText(stringValue);
            }
        } else {
            if (editorControl instanceof Text text) {
                if (!CommonUtils.equalObjects(text.getText(), stringValue)) {
                    text.setText(stringValue);
                }
            } else if (editorControl instanceof StyledText text) {
                if (!CommonUtils.equalObjects(text.getText(), stringValue)) {
                    text.setText(stringValue);
                }
            } else if (editorControl instanceof Button button) {
                button.setSelection(CommonUtils.toBoolean(value));
            } else if (editorControl instanceof Link link) {
                link.setData(value);
                link.setText(getLinkTitle(value));
            }
        }
    }

    private static String objectValueToString(Object value) {
        if (value instanceof DBPQualifiedObject qo) {
            return qo.getFullyQualifiedName(DBPEvaluationContext.UI);
        } if (value instanceof DBPNamedObject namedObject) {
            return namedObject.getName();
        } else if (value instanceof Enum<?> e) {
            return e.name();
        } else {
            return DBValueFormatting.getDefaultValueDisplayString(value, DBDDisplayFormat.EDIT);
        }
    }

    private static boolean isTextPropertyType(@Nullable Class<?> propertyType) {
        return propertyType == null || CharSequence.class.isAssignableFrom(propertyType) ||
            (propertyType.getComponentType() != null && CharSequence.class.isAssignableFrom(propertyType.getComponentType())) ||
            BeanUtils.isNumericType(propertyType);
    }

    @NotNull
    public List<DBPPropertyDescriptor> filterProperties(@NotNull DBPPropertyDescriptor[] props) {
        List<DBPPropertyDescriptor> result = new ArrayList<>();
        for (DBPPropertyDescriptor prop : props) {
            String category = prop.getCategory();
            if (!CommonUtils.isEmpty(category)) {
                // Keep only basic properties
                continue;
            }
            result.add(prop);
        }
        return result;
    }


    public boolean hasEditors() {
        return !editorMap.isEmpty();
    }

    public void clearEditors() {
        this.editorMap.clear();
        if (curButtonsContainer != null) {
            curButtonsContainer.dispose();
            curButtonsContainer = null;
        }
    }

}
